package com.panomedic.core;

import com.panomedic.utils.Utils;
import com.panomedic.*;
import com.panomedic.colors.ColorConstants;
import com.panomedic.core.blur.ConvolveFilter;
import com.panomedic.core.blur.GaussianFilter;
import com.panomedic.log4j.LogCategories;
import com.panomedic.log4j.Logger;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.Double;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.prefs.Preferences;

/**
 *
 * @author Yare
 */
public class LUT {

    static Logger logger = new Logger();
    /** Mapping values for both photos  */
    protected ArrayList<MappingValuesList>[] map1;
    protected ArrayList<MappingValuesList>[] map2;
    /** Accuracy of mapping - value by which is multiplied component value range
     *  to get final number of maaping values
     */
    protected int accuracy;
    /** Value ranges for all components */
    protected int[] range;
    protected Photo p1;
    protected Photo p2;
    protected final static double minValue = 0;
    protected Preferences prefs;

    /**
     * Standard constructor of LUT class
     * @param range
     * @param accuracy
     */
    public LUT(int[] range, int accuracy) {
        prefs = Preferences.userRoot().node(Constants.defNodePath);

        this.accuracy = accuracy;
        this.range = range;
        int[] size = new int[range.length];
        map1 = new ArrayList[range.length];
        map2 = new ArrayList[range.length];

        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                size[i] = range[i] * accuracy + 1;
                map1[i] = new ArrayList(size[i]);
                map2[i] = new ArrayList(size[i]);
                logger.debug(LogCategories.PREPROCESS, "LUT " + i + " created, size=" + size[i]);
                for (int j = 0; j < size[i]; j++) {
                    map1[i].add(j, new MappingValuesList());
                    map2[i].add(j, new MappingValuesList());
                }
            } else {
                size[i] = 0;
            }
        }
    }

    /**
     * Creates the lookup table from the intersection of two Photos given. Intersection determined by two Points.
     * @param p1
     * @param p2
     * @param topLeft
     * @param bottomRight
     * @return
     */
    public boolean create(Photo p1, Photo p2, Point topLeft, Point bottomRight) {

        if (p1 == null || p2 == null) {
            return false;
        }
        this.p1 = p1;
        this.p2 = p2;

//        DIVIDER
        double[] divider = new double[]{0.5, 0.5, 0.5};     // 0.0 for p1 only, 1.0 for p2 only

        Intersection in = new Intersection(p1, p2);
        in.determineTransFnc();

        double[] p1SD = in.getHist(p1).getSD();
        double[] p2SD = in.getHist(p2).getSD();

        Photo[] biggerSD = {null, null, null};
        double[] factorSD = new double[]{1, 1, 1};

        for (int i = 0; i < biggerSD.length; i++) {
            if (p1SD[i] < p2SD[i]) {
                biggerSD[i] = p2;
                if (p1SD[i] != 0) {
                    factorSD[i] = p2SD[i] / p1SD[i];
                }
            } else {
                biggerSD[i] = p1;
                if (p1SD[i] != 0) {
                    factorSD[i] = p1SD[i] / p2SD[i];
                }
            }
        }

        logger.debug(LogCategories.PREPROCESS, "FactorSD: " + Arrays.toString(factorSD) + "(" + Arrays.toString(biggerSD) + ")");
        int size = in.getSize();
        logger.debug(LogCategories.PREPROCESS, "size : " + size);

        double threshold = 0.10;
        int p1e = p1.getExtremeCount(topLeft, bottomRight, threshold);
        int p2e = p2.getExtremeCount(topLeft, bottomRight, threshold);
        double p1coef = (double) p1e / size;
        double p2coef = (double) p2e / size;

        logger.debug(LogCategories.PREPROCESS, "p1coef: " + p1coef);
        logger.debug(LogCategories.PREPROCESS, "p2coef: " + p2coef);

        for (int i = 0; i < factorSD.length; i++) {
            if (biggerSD[i] == p1) {
                divider[i] = p1coef / 2;
            } else {
                divider[i] = 1 - p2coef / 2;
            }
        }
        logger.debug(LogCategories.PREPROCESS, "Divider: " + Arrays.toString(divider));
//        ~DIVIDER

//        BLUR
        int radius = prefs.getInt(Constants.BLUR_RADIUS, 10);

        Point forFrom = new Point();
        Point forTo = new Point();
        Raster r1;
        Raster r2;

        //  blur 
        if (radius != 0) {
            ConvolveFilter gauss = new GaussianFilter(radius);

            BufferedImage img = p1.getImage();
            BufferedImage tmp = null;
            BufferedImage dst = null;
            tmp = img.getSubimage(topLeft.x, topLeft.y,
                    bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
            dst = gauss.filter(tmp, dst);
            r1 = dst.getRaster();

            BufferedImage img2 = p2.getImage();
            BufferedImage dst2 = null;
            BufferedImage tmp2 = img2.getSubimage(topLeft.x, topLeft.y,
                    bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
            dst2 = gauss.filter(tmp2, dst2);
            r2 = dst2.getRaster();

            forFrom.x = 0;
            forTo.x = r1.getWidth();
            forFrom.y = 0;
            forTo.y = r1.getHeight();
        } // not blur
        else {
            r1 = p1.getImage().getRaster();
            r2 = p2.getImage().getRaster();

            forFrom.x = topLeft.x;
            forFrom.y = topLeft.y;
            forTo.x = bottomRight.x;
            forTo.y = bottomRight.y;
        }
        // ~blurring

        int[] px1 = null;
        int[] px2 = null;

        int csType = prefs.getInt(Constants.CS_TYPE, ColorConstants.HSV);

        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 4))[i]) {

                logger.info(LogCategories.PREPROCESS, "Mapping component " + i + "...");
                for (int x = forFrom.x; x < forTo.x; x++) {
                    for (int y = forFrom.y; y < forTo.y; y++) {
//                for (int x = topLeft.x; x < bottomRight.x; x++) {
//                    for (int y = topLeft.y; y < bottomRight.y; y++) {
                        try {
                            // DEBUG
                            if (i == 2) {
                                int brea = 3;
                            }
                            // ~DEBUG
                            px1 = r1.getPixel(x, y, px1);
                            px2 = r2.getPixel(x, y, px2);


                            double[] px1con = Utils.getPixelValues(px1, csType);
                            double[] px2con = Utils.getPixelValues(px2, csType);

                            int position1 = (int) Math.round(px1con[i] * accuracy);
                            int position2 = (int) Math.round(px2con[i] * accuracy);
                            double value = px1con[i] + (px2con[i] - px1con[i]) * divider[i];
                            map1[i].get(position1).add(value);
                            map2[i].get(position2).add(value);

                        } catch (Exception e) {
                            logger.error(LogCategories.PREPROCESS, "ERROR: x: " + x + " y: " + y + " topLeft: " + topLeft + " bottomRight: " + bottomRight);
                            logger.error(LogCategories.PREPROCESS, e.toString());
                        }

                    }   //  y
                }   //  x
            }   //  component processed
        }   //  for components
//        this.print(map1);
//        this.print(map2);
        this.merge();
        this.interpolate(map1);
        this.interpolate(map2);

        this.smoothenMedian();

        int smooth = prefs.getInt(Constants.SMOOTHING, 0);
        switch (smooth) {
            case (1):
                this.smoothenAverage();
                break;
            case (2):
                this.smoothenMedian();
                break;
            default:
                break;
        }
        if (prefs.getBoolean(Constants.MONOTON, false)) {
            this.makeIncreasing();
        }
//        this.print(map1);
//        this.print(map2);
//        this.print2file(map1, new File("E:\\lut1_o.txt"));
//        this.print2file(map2, new File("E:\\lut2_o.txt"));
//        this.loadFromFile(map1, new File("E:\\LUT-Bezier\\lut1-Bezier7.txt"));
//        this.loadFromFile(map2, new File("E:\\LUT-Bezier\\lut2-Bezier5.txt"));



        return true;
    }

    /**
     * Loads map[0] value from text file - list of values separated with \n
     * @param map
     * @param file
     */
    private void loadFromFile(ArrayList<MappingValuesList>[] map, File file) {
        try {
            Scanner fileScan = new Scanner(file);
            fileScan.useDelimiter("\n");
            int count = 0;
            while (fileScan.hasNext() && count < map[0].size()) {
                double value = Double.parseDouble(fileScan.next());
//                System.out.println(fileScan.next());
                if (map[0] == null || map[0].get(count).isEmpty()) {
                    logger.error(LogCategories.PREPROCESS, "BAD FILE INPUT !!");
                }
                map[0].get(count++).set(0, value / 10);
            }
        } catch (IOException ex) {
            System.out.println(ex.toString());
        }
    }

    /**
     * Merges values in both LUTs gained in constructor 
     * - implemented as average of all values
     */
    private void merge() {
        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                logger.info(LogCategories.PROCESSING, "Merging component " + i + "...");
                for (int j = 0; j < map1[i].size(); j++) {
                    map1[i].get(j).setAverage();
                    map2[i].get(j).setAverage();
                }
            }
        }
        logger.info(LogCategories.PREPROCESS, "Done.");
    }

    /**
     * Interpolates missing values of LUT
     * - simple linear interpolation implemented
     */
    private void interpolate(ArrayList<MappingValuesList>[] map) {
        logger.info(LogCategories.PREPROCESS, "Interpolating " + getName(map));
        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                logger.info(LogCategories.PREPROCESS, "Interpolating component " + i + "...");
                int nSmaller = 0;
                int nGreater = accuracy * range[i] - 1;
                if (map[i].get(0).isEmpty()) {
                    map[i].get(0).add(minValue);
                }
                nGreater = findValidGreater(0, i, map);
                for (int j = 1; j < map[i].size(); j++) {
                    if (map[i].get(j).isEmpty()) {
                        double value = 0;
                        try {
                            double Y1 = (Double) map[i].get(nSmaller).get(0);
                            double Y2 = (Double) map[i].get(nGreater).get(0);
                            value = (Double) map[i].get(nSmaller).get(0) + (double) (j - nSmaller) / (nGreater - nSmaller) * (Y2 - Y1);
                        } catch (Exception e) {
                            logger.error(LogCategories.PREPROCESS, "component " + i + " interpolated value " + j + " nSmaller " + nSmaller + " nGreater " + nGreater);
                        }

                        map[i].get(j).add(value);
                    } else {
                        nSmaller = j;
                        nGreater = findValidGreater(j, i, map);
                    }
                }
            }
        }
        logger.info(LogCategories.PREPROCESS, "Done.");
    }

    /**
     * Finds the nearest index in the LUT after the given index, on which is valid value.
     * If there is not any, adds a maximum possible value to the last position and returns its index
     * @param index in LUT, after which we look for a valid value
     * @param component number of processed component (of the color space)
    private double getSmoothVal(double prev1, double val1) {
    throw new UnsupportedOperationException("Not yet implemented");
    }
     * @return
     */
    private int findValidGreater(int index, int component, ArrayList<MappingValuesList>[] map) {
        int pos = index + 1;
        while (pos < (range[component] * accuracy) && map[component].get(pos).isEmpty()) {
            pos++;
        }
        if (pos == (range[component] * accuracy) && map[component].get(pos).isEmpty()) {
            map[component].get(pos).add(minValue + range[component]);
        }
        return pos;
    }

    /**
     * Prints the LUT to the standard output.
     */
    private void print(ArrayList<MappingValuesList>[] map) {
        logger.debug(LogCategories.PROCESSING, "LUT " + getName(map));
        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                logger.debug(LogCategories.PROCESSING, "\nLUT for component " + i + " : ");
                String debugRow = new String();
                for (int j = 0; j < map[i].size(); j++) {
                    double avg = map[i].get(j).getAverage();

                    avg = Math.round(avg * 100) / 100.0;
                    debugRow += j + ": ";
                    debugRow += avg;
                    if (j % 10 == 0) {
                        debugRow += "\n";
                    } else {
                        debugRow += " ";
                    }
                }
            }
        }
    }

    /**
     * Prints the LUT to the standard output.
     */
    private void print2file(ArrayList<MappingValuesList>[] map, File file) {
        try {
            FileWriter ds = new FileWriter(file);
            for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
                if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                    for (int j = 0; j < map[i].size(); j++) {
                        Integer avg = (int) (map[i].get(j).getAverage() * 10);
                        ds.append(avg.toString());
                        ds.append("\n");
                    }
                }
            }
            ds.close();
        } catch (IOException ex) {
            logger.error(LogCategories.PROCESSING, ex);
        }
    }

    /**
     * Returns a value from LUT, that corresponds to the given value
     * @param lut1 array value of color components
     * @param p Photo object which is wanted to map FROM
     * @return array of mapped color components
     */
    public double[] getMapping(double[] val1, Photo p) {
        ArrayList<MappingValuesList>[] map = null;
        if (p == p1) {
            map = map1;
        } else if (p == p2) {
            map = map2;
        }
        double[] val2 = new double[3];
        try {
            for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
                if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                    val2[i] = (Double) map[i].get((int) Math.round(val1[i] * accuracy)).get(0);
//                    System.err.println("ERROR: component "+i+" pixel array "+Arrays.toString(val1));
                } else {
                    val2[i] = val1[i];
                }
            }
        } catch (Exception ex) {
            logger.error(LogCategories.PROCESSING, ex.toString());
        }
        return val2;
    }

    /**
     * Returns a value from LUT, that corresponds to the given value
     * @param val1 array value of color components
     * @param component component of the pixel
     * @param p Photo object which is wanted to map FROM
     * @return array of mapped color components
     */
    public double getMapping(double val1, int component, Photo p) {
        ArrayList<MappingValuesList>[] map = null;
        if (p == p1) {
            map = map1;
        } else if (p == p2) {
            map = map2;
        }
        double val2 = 0;
        val2 = (double) map[component].get((int) (val1 * accuracy)).get(0);
        return val2;
    }

    public int getAccuracy() {
        return accuracy;
    }

    public int[] getRange() {
        return range;
    }

    public Photo getP1() {
        return p1;
    }

    public Photo getP2() {
        return p2;
    }

    public String getName(ArrayList<MappingValuesList>[] map) {
        if (map == map1) {
            return new String("LUT " + p1.toString() + " -> " + p2.toString());
        } else if (map == map2) {
            return new String("LUT " + p2.toString() + " -> " + p1.toString());
        } else {
            return null;
        }
    }

    private void makeIncreasing() {
        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            double prev1 = 0;
            double prev2 = 0;
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                logger.debug(LogCategories.PROCESSING, "Smoothing component " + i + "...");
                for (int j = 0; j < map1[i].size(); j++) {
                    double val1 = map1[i].get(j).get(0);
                    if (prev1 > val1) {
                        map1[i].get(j).set(0, prev1);
                    } else {
                        prev1 = val1;
                    }
                    double val2 = map2[i].get(j).get(0);
                    if (prev2 > val2) {
                        map2[i].get(j).set(0, prev2);
                    } else {
                        prev2 = val2;
                    }
                }
            }
        }
    }

    private void smoothenAverage() {

        int[] size = new int[3];
        for (int i = 0; i < size.length; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                size[i] = map1[i].size();
            }
        }
        int avgRange = 5;               // odd number !!
        int scaledRange = accuracy * avgRange;
        int halfScaledRange = scaledRange / 2;

        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                logger.debug(LogCategories.PROCESSING, "Smoothing component " + i + "...");

                ArrayList<Double> totals1 = new ArrayList<Double>();
                ArrayList<Double> totals2 = new ArrayList<Double>();
                double sum1 = 0;
                double sum2 = 0;
                for (int j = 0; j < map1[i].size(); j++) {
                    sum1 += map1[i].get(j).get(0);
                    totals1.add(sum1);
                    sum2 += map2[i].get(j).get(0);
                    totals2.add(sum2);
                }

                for (int j = 0; j < map1[i].size(); j++) {
                    int lower = j - halfScaledRange;
                    int upper = j + halfScaledRange;
                    int divider = scaledRange;

                    if (j < halfScaledRange) {
                        divider = j + scaledRange - halfScaledRange;
                        lower = 0;
                    } else if (j >= size[i] - halfScaledRange) {
                        divider = size[i] - j + halfScaledRange;
                        upper = size[i] - 1;
                    } else {
                    }
                    try {
                        double val = (totals1.get(upper) - totals1.get(lower)) / divider;
                        map1[i].get(j).set(0, val);

                        val = (totals2.get(upper) - totals2.get(lower)) / divider;
                        map2[i].get(j).set(0, val);
                    } catch (ArrayIndexOutOfBoundsException ex) {
                        logger.error(LogCategories.PREPROCESS, "Exception: component=" + i + ", item=" + j + ", lower=" + lower + ", upper=" + upper + ", div=" + divider + ", ex=" + ex.toString());
                    }
                }
            }
        }
    }

    /**
     * Smoothens the LUT curve by convolving with median filter
     */
    private void smoothenMedian() {

        int[] size = new int[3];
        for (int i = 0; i < size.length; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                size[i] = map1[i].size();
            }
        }

        int avgRange = 5;               // odd number !!
        int scaledRange = accuracy * avgRange;
        int halfScaledRange = scaledRange / 2;

        for (int i = 0; i < ColorConstants.NUM_COMP; i++) {
            if (Utils.getCompProc(prefs.getInt(Constants.COMP_PROC, 7))[i]) {
                logger.debug(LogCategories.PROCESSING, "Smoothing component " + i + "...");

                List<Double> floatingList1 = new ArrayList<Double>();
                List<Double> floatingList2 = new ArrayList<Double>();
                for (int j = 0; j < halfScaledRange + 1; j++) {
                    floatingList1.add(map1[i].get(0).get(0));
                    floatingList2.add(map2[i].get(0).get(0));
                }
                for (int j = 1; j < halfScaledRange; j++) {
                    floatingList1.add(map1[i].get(j).get(0));
                    floatingList2.add(map2[i].get(j).get(0));
                }

                List<Double> resList1 = new ArrayList<Double>();
                List<Double> resList2 = new ArrayList<Double>();

                for (int j = 0; j < map1[i].size(); j++) {

                    int lower = j - halfScaledRange;
                    int upper = j + halfScaledRange;

                    if (lower < 0) {
                        lower = 0;
                    }
                    if (upper >= size[i]) {
                        upper = size[i] - 1;
                    }

                    try {
                        floatingList1.add(map1[i].get(upper).get(0));
                        Collections.sort(floatingList1);
                        double val1 = floatingList1.get(halfScaledRange);
                        resList1.add(val1);
                        floatingList1.remove(map1[i].get(lower).get(0));

                        floatingList2.add(map2[i].get(upper).get(0));
                        Collections.sort(floatingList2);
                        double val2 = floatingList2.get(halfScaledRange);
                        resList2.add(val2);
                        floatingList2.remove(map2[i].get(lower).get(0));
                    } catch (Exception ex) {
                        logger.error(LogCategories.PREPROCESS, "Exception: component=" + i + ", item=" + j + ", lower=" + lower + ", upper=" + upper + ", ex=" + ex.toString());
                    }

                }
                for (int j = 0; j < map1[i].size(); j++) {
                    try {
                        map1[i].get(j).set(0, resList1.get(j));
                        map2[i].get(j).set(0, resList2.get(j));
                    } catch (Exception ex) {
                        logger.error(LogCategories.PREPROCESS, "Exception: (copy) component=" + i + ", item=" + j + ", ex=" + ex.toString());
                    }
                }

            }   //  if component
        }   //  for components
    }

    private double getSmoothVal(double prev1a, double prev1b, double prev1c, double val1) {
        return (prev1a + prev1b + prev1c + val1) / 4;
    }

    public static void main(String[] args) {
        LUT lut = new LUT(new int[]{100, 100, 100}, 100);
//        ArrayList<MappingValuesList>[] map1 = map1 = new ArrayList[range.length];
//        lut.loadFromFile(new File("E:\\LUT-Bezier\\lut1-Bezier5.txt"));

    }
}
